This is a tutorial loosely based on this writeup. That writeup describes using table table to implement test data setup to make determining expected results easier. You can read that for a slightly different take. That example was written after the fact and somewhat cleaned up. It also is not a tutorial; it is really a summary of what you'll be doing in this tutorial.
In this tutorial, you'll review the setup for a previous test and then build the test setup in a way that will much better relate to the domain. Unlike the original table table example, this one will seem a lot more like a plausible development effort.
Getting Started
As with the other tutorials, you can continue from the work you've done on the previous tutorial, or you can use the source and start at the tag: FitNesse.Tutorials.TableTables.Start.
Up to this point, you have created programs using several different styles. However, all of these styles are very different from the underlying domain. This tutorial picks up from the Scenario Tables Tutorial and looks at one final way to create a program guide, or a series of programs.
Creating Many Programs
You used the following table to populate the program schedule (this is a snippet):
|Create Daily Program Named|D5_1|On Channel|5|Starting On|3/4/2008|at|20:00|Length|30|Episodes|7|
|Create Daily Program Named|D5_2|On Channel|5|Starting On|3/4/2008|at|20:30|Length|30|Episodes|7|
|Create Daily Program Named|D5_3|On Channel|5|Starting On|3/4/2008|at|21:00|Length|30|Episodes|7|
|Create Daily Program Named|D5_4|On Channel|5|Starting On|3/4/2008|at|21:30|Length|30|Episodes|7|
|Create Daily Program Named|D6_1|On Channel|6|Starting On|3/4/2008|at|20:00|Length|30|Episodes|7|
|Create Daily Program Named|D6_2|On Channel|6|Starting On|3/4/2008|at|20:30|Length|30|Episodes|7|
|Create Daily Program Named|D6_3|On Channel|6|Starting On|3/4/2008|at|21:00|Length|30|Episodes|7|
|Create Daily Program Named|D6_4|On Channel|6|Starting On|3/4/2008|at|21:30|Length|30|Episodes|7|
...
What does this table represent? What follows is a different visualization of this same data to make it easier to understand. In fact, it is just two days, but you can probably imagine extending this to support multiple days:
Notice that this appears more like a program guide you might see with cable receiver or a DVR. This isn't perfect because there are artifacts from the original table in the forms of the names used to create the programs. Even so, this appears to be a bit closer to a program guide.
Next, consider the following simplifications:
Programs only have one episode called "E1"
Only consider one day
Simplify the names. The name of the program/episode is just a sequence of the same characters, e.g. aaaabbbb, represents 2 programs, one called aaaa and the second called bbbb.
Rather than specify each starting time, make the length of the name related to the program length, e.g., 4 letters is 1 hour, 1 letter is 15 minutes.
Why do this? When I was testing the logic of selecting the correct programs with multiple season passes and a variable number of recorders, I had trouble program the schedules using the script table. What I was doing in my head was visualizing the program guide used by the DVRs I've personally used. It then hit me that the visualization of the program guide was essential to my use of the DVR. My tests did not reflect that so it occurred to me to make my tests reflect that as close as I could.
There is one problem with this setup. On DVRs, the length of the program is not related to the length of its name. This is an artificial simplification to make the table representation look decent. Even so, in practice this representation made writing tests easier at the expense of a more complex fixture, and that's the right choice to make.
What you will do in the remainder of this tutorial is create a fixture to handle this new table type. Once you've done that, you'll recreate some of the tests from the previous tutorial using the table table for the setup.
Creating the table
As with the previous tutorials, you'll create these tests under their own sub-hierarchy:
Change its type to a suite page (Note, as of 4/15/09, FitNesse has been updated to automatically set the page type to a suite if it ends in "Examples". If you build from source or you happen to have a recent release with this feature built, you might not need to set the type to suite.)
Notice that this borrows the Scenario table and script table from the SetUp page in the previous tutorial. In this tutorial, the only new table is a new Table Table to populate the program schedule.
|Scenario|A Two Recorder Dvr With These Season Passes|seasonPasses|Should Have These Episodes In To Do List|toDoList|
|Dvr Can Simultaneously Record | 2 | And With These |@seasonPasses|Should Have The Following|@toDoList|
|A Two Recorder Dvr With These Season Passes Should Have These Episodes In To Do List|
|seasonPasses |toDoList |
|cccccccc:200,FF:302 |cccccccc:E:1-1,FF:E:1-1 |
You might need to set this page to a test page. As of 4/15/09, the FitNesse source will automatically set the test page type for pages that begin with or end with the word "Example" (in addition to the word "Test"). However, you might not have the latest release.
Finally, you have a complete test with SetUp and TearDown code. Run it and notice that the test fails. It cannot find the class "Create One Day Program Guide", which is required by the Table Table.
While this Fixture does not do anything yet, it is a minimal example that will get the test to run, finding all of the Fixtures. The test is still failing.
The minimal requirement for the fixture is a doTable method as shown above. Since the table takes in parameters to its constructor, this fixture also needs a matching constructor. Now that you have the basic infrastructure in place, it's time to experiment just a little bit to see just what FitNesse really passes in to the doTable method.
Does this look familiar? This gives a hint at just what a table-table does. FitNesse simply passes in the entire table (minus the first row) in the form of a list of a list of strings:
The outside list represents the collection of rows. The first entry is actually the second row of the actual table, FitNesse does not pass in the row naming the fixture. (We're goig to ignore that row altogether in this tutorial, it's there for documentation).
The inside list represents the individual cells within a given row. In our case, the first cell is the channel. The remaining cells represent an hour of programming.
With that basic understanding, now it is time to process an individual row. This fixture (and in general, table-table fixtures) can be complex enough to warrant unit test code. Why is that? You are trying to make a table that is easy for a non-programmer to be able to use effectively; something that is closer to the problem domain. Because the table is closer to the domain and further away from the implementation, it will require some amount of coding.
Switch to Unit Testing
Our fixture needs to be able process a series of rows, each of which represent the a channel of programming. That's where we'll start with unit testing.
Create the First Test
This first test simply puts most of the basic API in palce:
packagecom.om.example.dvr.fixtures;importstaticorg.junit.Assert.assertEquals;importjava.util.List;importorg.junit.Test;importcom.om.example.dvr.domain.Program;publicclass ProgramGuideRowParserTest {
@Test
publicvoid emptyRowGeneratesNoPrograms(){
ProgramGuideRowParser parser = new ProgramGuideRowParser();List<Program> result = parser.parse("");
assertEquals(0, result.size());}}
Purpose: The purpose of this test is to being defining the API by which row parsing will happen.
Note: Did you notice that you just added equals() methods to Program and TimeSlot without adding unit tests to those same classes? Is this a problem? These methods are complex enough that there is certainly some risk in adding them. Also, in Java it is standard practice to write hashCode() when writing equals() just in case the object is used as a key in a Map. However, the code does not sore these objects as keys in Maps, so writing a hashCode() method, while conventional, is not really necessary.
These methods were written in response to a test, something more than a unit test, but a test none the less. Whether to add tests for the equals() method beyond what we've already written is not a clear yes or no decision, so I'll leave that to the reader since this is more about working with FitNesse than unit testing (in the book version of this tutorial, however, I'll probably take the other approach).
Next Test: Getting Program Length Correct
Add this test:
@Test
publicvoid oneThirtyMinuteProgram()throwsParseException{
parser.setChannel(204);List<Program> result = parser.parse("|aa|");
assertEquals(1, result.size());
Program expected = buildProgram("4/8/2008", "1:00", "aa", 204, 30);
assertEquals(expected, result.get(0));}
Run it and verify that it does not pass.
Next, update the production code in ProgramGuideRowParser:
Note: This is a somewhat refactored method. It will get longer and shorter as you work through this parsing exercise.
Next Test: Handle two 30 minute programs
Add a new test (and update the @Before method):
@Before
publicvoid init()throwsParseException{
parser = new ProgramGuideRowParser(DateUtil.instance()
.buildDate("4/8/2008", "1:00"));
parser.setChannel(204);}// ...
@Test
publicvoid twoThirtyMinuteProgramsInSameCell()throwsParseException{List<Program> result = parser.parse("|aabb|");
assertEquals(2, result.size());
Program expected0 = buildProgram("4/8/2008", "1:00", "aa", 204, 30);
assertEquals(expected0, result.get(0));
Program expected1 = buildProgram("4/8/2008", "1:30", "bb", 204, 30);
assertEquals(expected1, result.get(1));}
Note: You can remove the parser.setChannel(204) from the other two @Test methods since that duplicated method has be pushed up into the @Before method.
Run your tests, the new one will.
Now make several updates to make this next test pass (and notice that the code is getting unruly):
FitNesse removes extra spaces on either side of the cell. To represent "no program", the example uses _. Here's a test that verifies the production code handles _'s correctly:
@Test
publicvoid underscoredIgnored()throwsParseException{List<Program> result = parser.parse("|__aa|");
assertEquals(1, result.size());
Program expected = buildProgram("4/8/2008", "1:30", "aa", 204, 30);
assertEquals(expected, result.get(0));}
After adding this test, verify that it fails.
Now, update the parser to handle this condition (note, this is after several rounds of refactoring, with much refactoring still left):
packagecom.om.example.dvr.fixtures;importjava.text.ParseException;importjava.util.Date;importcom.om.example.dvr.domain.Program;importcom.om.example.dvr.domain.TimeSlot;importcom.om.example.util.DateUtil;publicclass ProgramBuilderUtil {publicstatic Program buildProgram(String date, String time, String name, int channel,
int duration)throwsParseException{Date expectedStartDateTime = DateUtil.instance().buildDate(date, time);returnnew Program(name, "E1", new TimeSlot(channel, expectedStartDateTime,
duration));}}
Also, the new class ProgramBuilderUtil was extracted from the previous class:
Update: ProgramGuideProgramCellsParserTest.java
private Program buildProgram(String date, String time, String name, int channel,
int duration)throwsParseException{return ProgramBuilderUtil.buildProgram(date, time, name, channel, duration);}
Run all of your unit tests, make sure everything passes.
Finally, upate the Table Fixture
All the pieces are in place, now it's just a matter of creating the programs:
I made some false starts. For example, rather than having the ProgramGuideRowParser take in a String, it could easily have taken in a List<String> and did all of its work based on that. Also, the DvrRecording fixture requires a range of episodes, but the way this program fixture works, it only creates one episode per program. I'm sure you could review the fixtures and also the unit tests and production code and find several more places to refactor. Before you leave this tutorial, let's finish up with some basic refactoring of the Fixtures.
Allow for a Single Episode
After a little experimentation with the DvrRecording fixture, I discovered a simple change that allows a single value rather than a range. I tried a few values and ran the tests after each time.
Make the following update to DvrRecording (note, only the return statement in each of these methods changed):
Run all of your unit tests, make sure they still pass.
Run all of your acceptance tests, make sure the still pass.
Update your acceptance test:
|A Two Recorder Dvr With These Season Passes Should Have These Episodes In To Do List|
|seasonPasses |toDoList |
|cccccccc:200,FF:302 |cccccccc:E:1,FF:E:1 |
Run your acceptance test, make sure they all pass. Close, but not quite.
Run your acceptance tests, make sure they still pass.
Conclusion and Summary
Congratulations, you have finished this tutorial.
This tutorial demonstrated that you can create tables reflecting a more natural or fluent style and then write more complex fixture code to support that style.
Once you created the table, you started creating a parser using TDD to build the parser from the ground up. While this tutorial did not show you all of the intermediate steps, it certainly did cover many of the major moving parts. Once you built most of the infrastructure, you tied it into the fixture and got the test passing.
Table tables are a great way to pass anything in that you'd like. You can also pass back a completely different table. That is not demonstrated here, this tutorial simply passes back an empty list so FitNesse disregards the return result, but it certainly is possible to pass back something different from the input table.
Notice that this tutorial was different from the previous tutorials in that you spent more time working on fixture-based code. Indeed, there was very little new code added to the production side. This is consistent with table fixtures, most of the work is in mapping the table representation into some kind of logical message. When you use table fixtures, you are making it easier for acceptance test writers to write better looking tests. You are taking the burden of the responsibility to make things work.
Assuming you've worked through the previous tutorials, you have now used all of the basic table types in Slim. Congratulations!
Table of Contents
Background
This is a tutorial loosely based on this writeup. That writeup describes using table table to implement test data setup to make determining expected results easier. You can read that for a slightly different take. That example was written after the fact and somewhat cleaned up. It also is not a tutorial; it is really a summary of what you'll be doing in this tutorial.In this tutorial, you'll review the setup for a previous test and then build the test setup in a way that will much better relate to the domain. Unlike the original table table example, this one will seem a lot more like a plausible development effort.
Getting Started
As with the other tutorials, you can continue from the work you've done on the previous tutorial, or you can use the source and start at the tag: FitNesse.Tutorials.TableTables.Start.Up to this point, you have created programs using several different styles. However, all of these styles are very different from the underlying domain. This tutorial picks up from the Scenario Tables Tutorial and looks at one final way to create a program guide, or a series of programs.
Creating Many Programs
You used the following table to populate the program schedule (this is a snippet):What does this table represent? What follows is a different visualization of this same data to make it easier to understand. In fact, it is just two days, but you can probably imagine extending this to support multiple days:
Notice that this appears more like a program guide you might see with cable receiver or a DVR. This isn't perfect because there are artifacts from the original table in the forms of the names used to create the programs. Even so, this appears to be a bit closer to a program guide.
Next, consider the following simplifications:
With these changes, read this program guide:
Why do this? When I was testing the logic of selecting the correct programs with multiple season passes and a variable number of recorders, I had trouble program the schedules using the script table. What I was doing in my head was visualizing the program guide used by the DVRs I've personally used. It then hit me that the visualization of the program guide was essential to my use of the DVR. My tests did not reflect that so it occurred to me to make my tests reflect that as close as I could.
There is one problem with this setup. On DVRs, the length of the program is not related to the length of its name. This is an artificial simplification to make the table representation look decent. Even so, in practice this representation made writing tests easier at the expense of a more complex fixture, and that's the right choice to make.
What you will do in the remainder of this tutorial is create a fixture to handle this new table type. Once you've done that, you'll recreate some of the tests from the previous tutorial using the table table for the setup.
Creating the table
As with the previous tutorials, you'll create these tests under their own sub-hierarchy:Notice that this borrows the Scenario table and script table from the SetUp page in the previous tutorial. In this tutorial, the only new table is a new Table Table to populate the program schedule.
Next, we need a test that uses this http://localhost:8080/FrontPage.DigitalVideoRecorderExamples.TableTableExamples.SetUp
Create the Fixture
While this Fixture does not do anything yet, it is a minimal example that will get the test to run, finding all of the Fixtures. The test is still failing.
The minimal requirement for the fixture is a doTable method as shown above. Since the table takes in parameters to its constructor, this fixture also needs a matching constructor. Now that you have the basic infrastructure in place, it's time to experiment just a little bit to see just what FitNesse really passes in to the doTable method.
Does this look familiar? This gives a hint at just what a table-table does. FitNesse simply passes in the entire table (minus the first row) in the form of a list of a list of strings:
With that basic understanding, now it is time to process an individual row. This fixture (and in general, table-table fixtures) can be complex enough to warrant unit test code. Why is that? You are trying to make a table that is easy for a non-programmer to be able to use effectively; something that is closer to the problem domain. Because the table is closer to the domain and further away from the implementation, it will require some amount of coding.
Switch to Unit Testing
Our fixture needs to be able process a series of rows, each of which represent the a channel of programming. That's where we'll start with unit testing.Create the First Test
- This first test simply puts most of the basic API in palce:
Purpose: The purpose of this test is to being defining the API by which row parsing will happen.- Run the test, make sure it passes.
- Next, add another test with one program and notice that this requires several changes:
Update: ProgramGuideRowParserTest, Add new testUpdate ProgramGuidRowPaser: Add constructor and method
- Run your tests, they fail.
- Add missing equals() methods:
Update: TimeSlot:Update: Program
- Run your tests, they should pass.
Note: Did you notice that you just added equals() methods to Program and TimeSlot without adding unit tests to those same classes? Is this a problem? These methods are complex enough that there is certainly some risk in adding them. Also, in Java it is standard practice to write hashCode() when writing equals() just in case the object is used as a key in a Map. However, the code does not sore these objects as keys in Maps, so writing a hashCode() method, while conventional, is not really necessary.These methods were written in response to a test, something more than a unit test, but a test none the less. Whether to add tests for the equals() method beyond what we've already written is not a clear yes or no decision, so I'll leave that to the reader since this is more about working with FitNesse than unit testing (in the book version of this tutorial, however, I'll probably take the other approach).
Next Test: Getting Program Length Correct
- Run your tests, make sure they pass.
Note: This is a somewhat refactored method. It will get longer and shorter as you work through this parsing exercise.Next Test: Handle two 30 minute programs
- Add a new test (and update the @Before method):
Note: You can remove the parser.setChannel(204) from the other two @Test methods since that duplicated method has be pushed up into the @Before method.- Run your tests, the new one will.
- Now make several updates to make this next test pass (and notice that the code is getting unruly):
Update: ProgramGuideRowParser.javaUpdate: DateUtil.java
Next Test: Ignore _ in name
Next Test: A cell with all spaces handled correctly
Final Test: One Big Row
This algorithm is either close to complete or complete. Here's a final test that will bring everything together:Bringing it all together
Now that you can parse a single row, there are a few things left before your table-table fixture will be ready:This will require several more steps, so let's get started.
Refactor: The Name is wrong
The name of the parser class is wrong, it only parses the programs not the whole row (it does not handle the channel).Create the Real ProgramGuideRowParser
- This requires two new classes:
Create: ProgramBuilderUtil.javaCreate: ProgramGuideRowParser
- Also, the new class ProgramBuilderUtil was extracted from the previous class:
Update: ProgramGuideProgramCellsParserTest.javaFinally, upate the Table Fixture
All the pieces are in place, now it's just a matter of creating the programs:Final Cleanup
I made some false starts. For example, rather than having the ProgramGuideRowParser take in a String, it could easily have taken in a List<String> and did all of its work based on that. Also, the DvrRecording fixture requires a range of episodes, but the way this program fixture works, it only creates one episode per program. I'm sure you could review the fixtures and also the unit tests and production code and find several more places to refactor. Before you leave this tutorial, let's finish up with some basic refactoring of the Fixtures.Allow for a Single Episode
After a little experimentation with the DvrRecording fixture, I discovered a simple change that allows a single value rather than a range. I tried a few values and ran the tests after each time.Change ProgramGuideRowParser to take a List<String> instead of a String
Conclusion and Summary
Congratulations, you have finished this tutorial.This tutorial demonstrated that you can create tables reflecting a more natural or fluent style and then write more complex fixture code to support that style.
Once you created the table, you started creating a parser using TDD to build the parser from the ground up. While this tutorial did not show you all of the intermediate steps, it certainly did cover many of the major moving parts. Once you built most of the infrastructure, you tied it into the fixture and got the test passing.
Table tables are a great way to pass anything in that you'd like. You can also pass back a completely different table. That is not demonstrated here, this tutorial simply passes back an empty list so FitNesse disregards the return result, but it certainly is possible to pass back something different from the input table.
Notice that this tutorial was different from the previous tutorials in that you spent more time working on fixture-based code. Indeed, there was very little new code added to the production side. This is consistent with table fixtures, most of the work is in mapping the table representation into some kind of logical message. When you use table fixtures, you are making it easier for acceptance test writers to write better looking tests. You are taking the burden of the responsibility to make things work.
Assuming you've worked through the previous tutorials, you have now used all of the basic table types in Slim. Congratulations!
<--Back -or- Next Tutorial-->